Skip to content

perf: drop task wrapper in EventNamespace.emit_update, keep flush tick#6734

Open
Alek99 wants to merge 2 commits into
mainfrom
claude/reflex-perf-optimizations-21kg8y-emit-update
Open

perf: drop task wrapper in EventNamespace.emit_update, keep flush tick#6734
Alek99 wants to merge 2 commits into
mainfrom
claude/reflex-perf-optimizations-21kg8y-emit-update

Conversation

@Alek99

@Alek99 Alek99 commented Jul 10, 2026

Copy link
Copy Markdown
Member

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?

EventNamespace.emit_update wrapped the socket emit in asyncio.create_task(...) and then immediately awaited it, with a comment claiming the task "prevents the update from being blocked behind other coroutines". Awaiting a freshly created task blocks the caller exactly like awaiting the coroutine directly — the wrapper never provided any concurrency; it only added per-update task creation/scheduling overhead (plus an f-string task name with a time.time() call). Since emit_update runs while the state lock is held (via modify_state / the event processor's emit_delta path), that overhead was paid on every outgoing state update.

The one load-bearing side effect of the wrapper (caught by test_yield_state_update[click_yield_interim_value] on the first iteration of this PR): awaiting a fresh task forces an event-loop tick, which lets engineio's per-connection writer task flush the queued packet before the caller resumes. sio.emit itself can complete without ever suspending (the packet is only put on a non-full queue), so a sync handler that blocks the loop right after yielding (e.g. yieldtime.sleep(...)) would otherwise hold the interim update hostage in the write queue. The fix keeps that guarantee explicitly with await asyncio.sleep(0) after the emit, with a regression note explaining why the tick must stay.

Micro-benchmark (Python 3.13, replicating the exact await patterns with a stand-in emit coroutine, 20k iterations after warmup):

variant per update
before: await asyncio.create_task(emit(...), name=f"...{time.time()}") 9.19 µs
after: await emit(...) + await asyncio.sleep(0) 4.84 µs

1.9x less overhead per outgoing update. A writer-task ordering simulation confirms the interim packet is flushed before a loop-blocking caller resumes in both the old and new versions (and is not flushed with a plain await emit(...) alone — which is why the sleep(0) is load-bearing).

Linear: ENG-10110

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sns7EuBPYvfGCxRSRZ1bUx

@Alek99 Alek99 requested a review from a team as a code owner July 10, 2026 19:25
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR simplifies state-update websocket emits while keeping the flush yield. The main changes are:

  • Directly awaits EventNamespace.emit_update socket emits instead of wrapping them in a task.
  • Adds an explicit event-loop yield after each emit so queued packets can flush.
  • Adds a performance news entry for the reduced per-update overhead.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
reflex/app.py Updates EventNamespace.emit_update to await the emit directly and yield one loop tick after queuing the packet.
news/6734.performance.md Adds a release note describing the emit-update scheduling overhead reduction.

Reviews (2): Last reviewed commit: "chore: add news fragment for #6734" | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/reflex-perf-optimizations-21kg8y-emit-update (846a0ee) with main (9a5c4d3)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Awaiting a freshly created task blocks the caller exactly like awaiting
the coroutine directly, so the wrapper never provided the claimed
non-blocking behavior -- it only added task creation/scheduling overhead
(~4-7us, 2-3x) on every outgoing state update.

The task wrapper did have one load-bearing side effect: it forced an
event loop tick, giving the engineio writer task a chance to flush the
queued packet before the caller resumes (important when a sync event
handler blocks the loop right after yielding an interim update). Keep
that guarantee with an explicit asyncio.sleep(0) after the emit, which
is still ~2x cheaper than the task wrapper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sns7EuBPYvfGCxRSRZ1bUx
@Alek99 Alek99 force-pushed the claude/reflex-perf-optimizations-21kg8y-emit-update branch from bab3d82 to 3763baa Compare July 10, 2026 19:34
@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

ENG-10110

@Alek99 Alek99 changed the title perf: drop pointless task wrapper in EventNamespace.emit_update perf: drop task wrapper in EventNamespace.emit_update, keep flush tick Jul 10, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sns7EuBPYvfGCxRSRZ1bUx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants